package org.apache.lucene.search.spans;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.util.PriorityQueue;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.HashSet;

/**
 * Similar to {@link NearSpansOrdered}, but for the unordered case.
 * 
 * Expert:
 * Only public for subclassing.  Most implementations should not need this class
 */
public class NearSpansUnordered extends Spans {
    private SpanNearQuery query;

    private List<SpansCell> ordered = new ArrayList<SpansCell>(); // spans in query order
    private Spans[] subSpans;
    private int slop; // from query

    private SpansCell first; // linked list of spans
    private SpansCell last; // sorted by doc only

    private int totalLength; // sum of current lengths

    private CellQueue queue; // sorted queue of spans
    private SpansCell max; // max element in queue

    private boolean more = true; // true iff not done
    private boolean firstTime = true; // true before first next()

    private class CellQueue extends PriorityQueue<SpansCell> {
        public CellQueue(int size) {
            initialize(size);
        }

        @Override
        protected final boolean lessThan(SpansCell spans1, SpansCell spans2) {
            if (spans1.doc() == spans2.doc()) {
                return NearSpansOrdered.docSpansOrdered(spans1, spans2);
            } else {
                return spans1.doc() < spans2.doc();
            }
        }
    }

    /** Wraps a Spans, and can be used to form a linked list. */
    private class SpansCell extends Spans {
        private Spans spans;
        private SpansCell next;
        private int length = -1;
        private int index;

        public SpansCell(Spans spans, int index) {
            this.spans = spans;
            this.index = index;
        }

        @Override
        public boolean next() throws IOException {
            return adjust(spans.next());
        }

        @Override
        public boolean skipTo(int target) throws IOException {
            return adjust(spans.skipTo(target));
        }

        private boolean adjust(boolean condition) {
            if (length != -1) {
                totalLength -= length; // subtract old length
            }
            if (condition) {
                length = end() - start();
                totalLength += length; // add new length

                if (max == null || doc() > max.doc() || (doc() == max.doc()) && (end() > max.end())) {
                    max = this;
                }
            }
            more = condition;
            return condition;
        }

        @Override
        public int doc() {
            return spans.doc();
        }

        @Override
        public int start() {
            return spans.start();
        }

        @Override
        public int end() {
            return spans.end();
        }

        // TODO: Remove warning after API has been finalized
        @Override
        public Collection<byte[]> getPayload() throws IOException {
            return new ArrayList<byte[]>(spans.getPayload());
        }

        // TODO: Remove warning after API has been finalized
        @Override
        public boolean isPayloadAvailable() {
            return spans.isPayloadAvailable();
        }

        @Override
        public String toString() {
            return spans.toString() + "#" + index;
        }
    }

    public NearSpansUnordered(SpanNearQuery query, IndexReader reader) throws IOException {
        this.query = query;
        this.slop = query.getSlop();

        SpanQuery[] clauses = query.getClauses();
        queue = new CellQueue(clauses.length);
        subSpans = new Spans[clauses.length];
        for (int i = 0; i < clauses.length; i++) {
            SpansCell cell = new SpansCell(clauses[i].getSpans(reader), i);
            ordered.add(cell);
            subSpans[i] = cell.spans;
        }
    }

    public Spans[] getSubSpans() {
        return subSpans;
    }

    @Override
    public boolean next() throws IOException {
        if (firstTime) {
            initList(true);
            listToQueue(); // initialize queue
            firstTime = false;
        } else if (more) {
            if (min().next()) { // trigger further scanning
                queue.updateTop(); // maintain queue
            } else {
                more = false;
            }
        }

        while (more) {

            boolean queueStale = false;

            if (min().doc() != max.doc()) { // maintain list
                queueToList();
                queueStale = true;
            }

            // skip to doc w/ all clauses

            while (more && first.doc() < last.doc()) {
                more = first.skipTo(last.doc()); // skip first upto last
                firstToLast(); // and move it to the end
                queueStale = true;
            }

            if (!more)
                return false;

            // found doc w/ all clauses

            if (queueStale) { // maintain the queue
                listToQueue();
                queueStale = false;
            }

            if (atMatch()) {
                return true;
            }

            more = min().next();
            if (more) {
                queue.updateTop(); // maintain queue
            }
        }
        return false; // no more matches
    }

    @Override
    public boolean skipTo(int target) throws IOException {
        if (firstTime) { // initialize
            initList(false);
            for (SpansCell cell = first; more && cell != null; cell = cell.next) {
                more = cell.skipTo(target); // skip all
            }
            if (more) {
                listToQueue();
            }
            firstTime = false;
        } else { // normal case
            while (more && min().doc() < target) { // skip as needed
                if (min().skipTo(target)) {
                    queue.updateTop();
                } else {
                    more = false;
                }
            }
        }
        return more && (atMatch() || next());
    }

    private SpansCell min() {
        return queue.top();
    }

    @Override
    public int doc() {
        return min().doc();
    }

    @Override
    public int start() {
        return min().start();
    }

    @Override
    public int end() {
        return max.end();
    }

    // TODO: Remove warning after API has been finalized
    /**
     * WARNING: The List is not necessarily in order of the the positions
     * @return Collection of <code>byte[]</code> payloads
     * @throws IOException
     */
    @Override
    public Collection<byte[]> getPayload() throws IOException {
        Set<byte[]> matchPayload = new HashSet<byte[]>();
        for (SpansCell cell = first; cell != null; cell = cell.next) {
            if (cell.isPayloadAvailable()) {
                matchPayload.addAll(cell.getPayload());
            }
        }
        return matchPayload;
    }

    // TODO: Remove warning after API has been finalized
    @Override
    public boolean isPayloadAvailable() {
        SpansCell pointer = min();
        while (pointer != null) {
            if (pointer.isPayloadAvailable()) {
                return true;
            }
            pointer = pointer.next;
        }

        return false;
    }

    @Override
    public String toString() {
        return getClass().getName() + "(" + query.toString() + ")@" + (firstTime ? "START" : (more ? (doc() + ":" + start() + "-" + end()) : "END"));
    }

    private void initList(boolean next) throws IOException {
        for (int i = 0; more && i < ordered.size(); i++) {
            SpansCell cell = ordered.get(i);
            if (next)
                more = cell.next(); // move to first entry
            if (more) {
                addToList(cell); // add to list
            }
        }
    }

    private void addToList(SpansCell cell) throws IOException {
        if (last != null) { // add next to end of list
            last.next = cell;
        } else
            first = cell;
        last = cell;
        cell.next = null;
    }

    private void firstToLast() {
        last.next = first; // move first to end of list
        last = first;
        first = first.next;
        last.next = null;
    }

    private void queueToList() throws IOException {
        last = first = null;
        while (queue.top() != null) {
            addToList(queue.pop());
        }
    }

    private void listToQueue() {
        queue.clear(); // rebuild queue
        for (SpansCell cell = first; cell != null; cell = cell.next) {
            queue.add(cell); // add to queue from list
        }
    }

    private boolean atMatch() {
        return (min().doc() == max.doc()) && ((max.end() - min().start() - totalLength) <= slop);
    }
}
